package com.stars.easyms.datasource.interceptor.mybatis;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.*;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.page.PageMethod;
import com.github.pagehelper.util.ExecutorUtil;
import com.github.pagehelper.util.MSUtils;
import com.stars.easyms.datasource.EasyMsDataSource;
import com.stars.easyms.datasource.EasyMsMultiDataSource;
import com.stars.easyms.datasource.EasyMsQueryExecutor;
import com.stars.easyms.datasource.bean.PageCountDelayHandleBean;
import com.stars.easyms.datasource.holder.EasyMsSynchronizationManager;
import com.stars.easyms.datasource.holder.EasyMsInterceptorSkipHolder;
import com.stars.easyms.datasource.pagehelper.EasyMsPageHelper;
import com.stars.easyms.datasource.page.PageParameter;
import com.stars.easyms.base.util.DefaultThreadFactory;
import com.stars.easyms.base.util.EasyMsThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.core.annotation.Order;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.*;

/**
 * 优化pageHelper的分页查询，查询总数使用异步查询，支持ByPaging结尾分页，若PageHelper.startPage和ByPaging同时存在，PageHelper.startPage优先级更高
 *
 * @author guoguifang
 * @date 2018-10-19 10:07
 * @since 1.0.0
 */
@Slf4j
@Order(2)
@SuppressWarnings({"rawtypes"})
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public final class EasyMsPageInterceptor extends PageInterceptor {

    private EasyMsPageHelper dialect;

    private static final String DEFAULT_PAGE_SQL_ID = ".*ByPaging$";

    private static final String DEFAULT_COUNT_ID_SUFFIX = "_COUNT";

    private final Configuration configuration;

    private final EasyMsMultiDataSource easyMsMultiDataSource;

    private static final ExecutorService executorService = new EasyMsThreadPoolExecutor(10, 100, 120, TimeUnit.SECONDS, new DefaultThreadFactory().setNameFormat("easyms-asynPageCount-%d").build());

    private final EasyMsQueryExecutor easyMsQueryExecutor = EasyMsQueryExecutor.getInstance();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 判断过滤器是否需要直接跳过
        if (EasyMsInterceptorSkipHolder.isSkip()) {
            return invocation.proceed();
        }

        boolean isNotEasyMsRepository = false;
        try {
            // 获取参数
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }

            // 判断是否确定数据源，如果未确定则表示为非EasyMsRepository注解的dao，则获取当前数据源并设置到holder中
            EasyMsDataSource currentDataSource = EasyMsSynchronizationManager.getEasyMsDataSource();
            if (currentDataSource == null) {
                currentDataSource = easyMsMultiDataSource.getCurrentDataSource();
                EasyMsSynchronizationManager.setEasyMsDataSource(currentDataSource);
                isNotEasyMsRepository = true;
            }
            dialect.initDelegateDialect(currentDataSource);

            // 分页标识，判断是否使用PageHelper.startPage的方式分页
            boolean pageFlag = !dialect.skip(ms, parameter, rowBounds);

            // 如果没有使用PageHelper.startPage的方式分页则判断是否以DEFAULT_PAGE_SQL_ID结尾，该方法用来兼容某些框架
            Object pageObject = null;
            PageParameter pageParameter = null;
            if (!pageFlag && ms.getId().matches(DEFAULT_PAGE_SQL_ID)) {
                Object parameterObject = boundSql.getParameterObject();
                if (parameterObject instanceof Map) {
                    Map parameterMap = (Map) parameterObject;
                    pageObject = parameterMap.get("page");
                    if (pageObject != null && !(pageObject instanceof String)) {
                        // 为了防止pageObject对象pageSize或currentPage使用long类型此处使用json转换，不使用BeanUtils
                        try {
                            JSONObject pageJsonObject = JSON.parseObject(JSON.toJSONString(pageObject));
                            Object pageSizeObject = pageJsonObject.get("pageSize");
                            Object currentPageObject = pageJsonObject.get("currentPage");
                            if (pageSizeObject != null && currentPageObject != null) {
                                // 获取当前页及页大小，如果都大于0则分页
                                int currentPage = Integer.parseInt(currentPageObject.toString());
                                int pageSize = Integer.parseInt(pageSizeObject.toString());
                                if (currentPage > 0 && pageSize > 0) {
                                    PageMethod.startPage(currentPage, pageSize);

                                    // 设置分页参数并在线程内保存原分页参数
                                    pageParameter = new PageParameter();
                                    pageParameter.setCurrentPage(currentPage);
                                    pageParameter.setPageSize(pageSize);
                                    pageFlag = !dialect.skip(ms, parameter, rowBounds);
                                }
                            }
                        } catch (Exception e) {
                            if (log.isDebugEnabled()) {
                                log.debug("Get page info fail!", e);
                            }
                        }
                    }
                }
            }

            // 根据分页标识获取查询结果
            List resultList;
            if (pageFlag) {
                resultList = executePageQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey, pageObject, pageParameter, currentDataSource);
            } else {
                resultList = easyMsQueryExecutor.executeQuery(executor, ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return EasyMsSynchronizationManager.getPageCountDelayHandleBean() != null ? resultList : dialect.afterPage(resultList, parameter, rowBounds);
        } finally {

            // 判断是否在本方法中设置了数据源，如果是并且是非同步管理的，则清除（为了防止内存泄露，threadLocal在哪里set就在哪里remove）
            if (isNotEasyMsRepository && !EasyMsSynchronizationManager.isSynchronization()) {
                EasyMsSynchronizationManager.clearEasyMsDataSource();
            }
            if (dialect != null && EasyMsSynchronizationManager.getPageCountDelayHandleBean() == null) {
                dialect.afterAll();
            }
        }
    }

    private List executePageQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql,
                                  CacheKey cacheKey, Object pageObject, PageParameter pageParameter, EasyMsDataSource currentDataSource) throws SQLException {
        Future<Long> countFuture = null;
        String countMsId = ms.getId() + DEFAULT_COUNT_ID_SUFFIX;
        if (dialect.beforeCount(ms, parameter, rowBounds)) {
            countFuture = executeCount(ms, countMsId, parameter, rowBounds, resultHandler, boundSql, currentDataSource);
        }
        List resultList = easyMsQueryExecutor.executePageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        if (countFuture != null) {
            PageCountDelayHandleBean bean = new PageCountDelayHandleBean(countMsId, countFuture, resultList.size(), pageObject, pageParameter, dialect, parameter, rowBounds);
            if (EasyMsSynchronizationManager.isSynchronization()) {
                EasyMsSynchronizationManager.setPageCountDelayHandleBean(bean);
            } else {
                easyMsQueryExecutor.handlePageCount(bean);
            }
        }
        return resultList;
    }

    private Future<Long> executeCount(MappedStatement ms, String countMsId, Object parameter, RowBounds rowBounds,
                                      ResultHandler resultHandler, BoundSql boundSql, EasyMsDataSource currentDataSource) {
        Callable<Long> callable;
        MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
        final MappedStatement countMappedStatement;
        final BoundSql fixedBoundSql = new BoundSql(ms.getConfiguration(), boundSql.getSql(), boundSql.getParameterMappings(), boundSql.getParameterObject());
        ExecutorUtil.getAdditionalParameter(boundSql).forEach(fixedBoundSql::setAdditionalParameter);
        if (countMs != null) {
            countMappedStatement = countMs;
            callable = () -> {
                boolean resetDataSource = false;
                try {
                    if (EasyMsSynchronizationManager.getEasyMsDataSource() == null) {
                        EasyMsSynchronizationManager.setEasyMsDataSource(currentDataSource);
                        dialect.initDelegateDialect(currentDataSource);
                        resetDataSource = true;
                    }
                    return easyMsQueryExecutor.executeManualCount(configuration, countMappedStatement, parameter, fixedBoundSql, resultHandler);
                } finally {
                    if (resetDataSource) {
                        dialect.clearDelegateDialect();
                        EasyMsSynchronizationManager.clearEasyMsDataSource();
                    }
                }
            };
        } else {
            countMs = msCountMap.get(countMsId);
            if (countMs == null) {
                countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                msCountMap.put(countMsId, countMs);
            }
            countMappedStatement = countMs;
            Page sourcePage = PageMethod.getLocalPage();
            int pageNum = sourcePage.getPageNum();
            int pageSize = sourcePage.getPageSize();
            String countColumn = sourcePage.getCountColumn();
            callable = () -> {
                boolean resetPage = PageMethod.getLocalPage() == null;
                boolean resetDataSource = false;
                try {
                    if (resetPage) {
                        PageMethod.startPage(pageNum, pageSize).countColumn(countColumn);
                    }
                    if (EasyMsSynchronizationManager.getEasyMsDataSource() == null) {
                        EasyMsSynchronizationManager.setEasyMsDataSource(currentDataSource);
                        dialect.initDelegateDialect(currentDataSource);
                        resetDataSource = true;
                    }
                    return easyMsQueryExecutor.executeAutoCount(dialect, configuration, countMappedStatement, parameter, fixedBoundSql, rowBounds, resultHandler);
                } finally {
                    if (resetDataSource) {
                        dialect.clearDelegateDialect();
                        EasyMsSynchronizationManager.clearEasyMsDataSource();
                    }
                    if (resetPage) {
                        PageMethod.clearPage();
                    }
                }
            };
        }
        return executorService.submit(callable);
    }

    @Override
    public void setProperties(Properties properties) {
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        this.dialect = new EasyMsPageHelper(properties);
    }

    public EasyMsPageInterceptor(Configuration configuration, EasyMsMultiDataSource easyMsMultiDataSource) {
        this.configuration = configuration;
        this.easyMsMultiDataSource = easyMsMultiDataSource;
        this.setProperties(new Properties());
    }
}
